/*
 *  Licensed to the Apache Software Foundation (ASF) under one or more
 *  contributor license agreements.  See the NOTICE file distributed with
 *  this work for additional information regarding copyright ownership.
 *  The ASF licenses this file to You under the Apache License, Version 2.0
 *  (the "License"); you may not use this file except in compliance with
 *  the License.  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 *  Unless required by applicable law or agreed to in writing, software
 *  distributed under the License is distributed on an "AS IS" BASIS,
 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *  See the License for the specific language governing permissions and
 *  limitations under the License.
 */

/**
 * @author Vladimir N. Molotkov, Stepan M. Mishura
 */

package org.apache.harmony.security.asn1;

import java.io.IOException;

import org.apache.harmony.security.internal.nls.Messages;

/**
 * Implicitly tagged ASN.1 type.
 * 
 * @see http://asn1.elibel.tm.fr/en/standards/index.htm
 */
public class ASN1Implicit extends ASN1Type {

	// primitive type of tagging
	private static final int TAGGING_PRIMITIVE = 0;

	// constructed type of tagging
	private static final int TAGGING_CONSTRUCTED = 1;

	// string type of tagging
	private static final int TAGGING_STRING = 2;

	// tagged ASN.1 type
	private final ASN1Type type;

	// type of tagging. There are three of them
	// 1) primitive: only primitive identifier is valid
	// 2) constructed: only constructed identifier is valid
	// 3) string: both identifiers are valid
	private final int taggingType;

	/**
	 * Constructs implicitly tagged ASN.1 type with context-specific tag class
	 * and specified tag number.
	 * 
	 * @param tagNumber
	 *            - ASN.1 tag number
	 * @param type
	 *            - ASN.1 type to be tagged
	 * @throws IllegalArgumentException
	 *             - if tagNumber or type is invalid
	 */
	public ASN1Implicit(int tagNumber, ASN1Type type) {
		this(CLASS_CONTEXTSPECIFIC, tagNumber, type);
	}

	/**
	 * Constructs implicitly tagged ASN.1 type
	 * 
	 * @param tagClass
	 *            - ASN.1 tag class.
	 * @param tagNumber
	 *            - ASN.1 tag number
	 * @param type
	 *            - ASN.1 type to be tagged
	 * @throws IllegalArgumentException
	 *             - if tagNumber, tagClass or type is invalid
	 */
	public ASN1Implicit(int tagClass, int tagNumber, ASN1Type type) {
		super(tagClass, tagNumber);

		if ((type instanceof ASN1Choice) || (type instanceof ASN1Any)) {
			// According to X.680:
			// 'The IMPLICIT alternative shall not be used if the type
			// defined by "Type" is an untagged choice type or an
			// untagged open type'
			throw new IllegalArgumentException(
					Messages.getString("security.9F")); //$NON-NLS-1$
		}

		this.type = type;

		if (type.checkTag(type.id)) {
			if (type.checkTag(type.constrId)) {
				// the base encoding can be primitive ot constructed
				// use both encodings
				taggingType = TAGGING_STRING;
			} else {
				// if the base encoding is primitive use primitive encoding
				taggingType = TAGGING_PRIMITIVE;
			}
		} else {
			// if the base encoding is constructed use constructed encoding
			taggingType = TAGGING_CONSTRUCTED;
		}
	}

	//
	//
	// Decode
	//
	//

	/**
	 * TODO
	 */
	@Override
	public final boolean checkTag(int identifier) {
		switch (taggingType) {
		case TAGGING_PRIMITIVE:
			return id == identifier;
		case TAGGING_CONSTRUCTED:
			return constrId == identifier;
		default: // TAGGING_STRING
			return id == identifier || constrId == identifier;
		}
	}

	/**
	 * TODO
	 */
	@Override
	public Object decode(BerInputStream in) throws IOException {
		if (!checkTag(in.tag)) {
			// FIXME need look for tagging type
			throw new ASN1Exception(Messages.getString("security.100", //$NON-NLS-1$
					new Object[] { in.tagOffset, Integer.toHexString(id),
							Integer.toHexString(in.tag) }));
		}

		// substitute indentifier for further decoding
		if (id == in.tag) {
			in.tag = type.id;
		} else {
			in.tag = type.constrId;
		}
		in.content = type.decode(in);

		if (in.isVerify) {
			return null;
		}
		return getDecodedObject(in);
	}

	//
	//
	// Encode
	//
	//

	@Override
	public void encodeASN(BerOutputStream out) {
		// FIXME need another way for specifying identifier to be encoded
		if (taggingType == TAGGING_CONSTRUCTED) {
			out.encodeTag(constrId);
		} else {
			out.encodeTag(id);
		}
		encodeContent(out);
	}

	@Override
	public void encodeContent(BerOutputStream out) {
		type.encodeContent(out);
	}

	@Override
	public void setEncodingContent(BerOutputStream out) {
		type.setEncodingContent(out);
	}
}